﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FStore.Formats;
using FStore.Utils;
using System.IO;
using System.ComponentModel;
using System.Collections;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace FStore
{
    public sealed class FlatStore
    {
        private string filePath = string.Empty;
        private FileStream fStream = null;
        private StreamReader sReader = null;
        private StreamWriter sWriter = null;

        private List<Type> types = new List<Type>();
        private Dictionary<Type, IFlatFormat> formats = new Dictionary<Type, IFlatFormat>();

        internal List<DataItem> items = new List<DataItem>();

        public event EventHandler StoreLoaded;

        private int domainForInstance = -1;

        private Type defaultFormat = null;

        private FlatStore()
        {
            // Check if the FlatStore instances domain is available
            if (Unique.IsDomainReservedRandomInt32(consts.FStoreInstances_Domain) == false)
            {
                // Reserve it
                Unique.ReserveInt32RandomDomain(consts.FStoreInstances_Domain);
            }

            bool bGotADomain = false;

            // Loop to find this instance it's own domain
            while (!bGotADomain)
            {
                // Get a random domain number from the instances domain
                domainForInstance = Unique.GetRandomInt32(consts.FStoreInstances_Domain);

                // See if a domain with the number is reserved
                if (Unique.IsDomainReservedRandomInt32(domainForInstance) == false)
                {
                    // It isn't, reserve it and we can use that for this instance
                    Unique.ReserveInt32RandomDomain(domainForInstance);
                    break;
                }

                // Free up the number we got from the instance domain and loop again
                Unique.RemoveRandomInt32(domainForInstance, consts.FStoreInstances_Domain);
            }

            Created = false;
        }

        private bool Created { get; set; }

        ~FlatStore()
        {
            // Unreserve this instances domain
            Unique.UnreserveInt32RandomDomain(domainForInstance);

            try
            {
                if (fStream != null)
                    fStream.Dispose();

                if (sReader != null)
                    sReader.Dispose();

                if (sWriter != null)
                    sWriter.Dispose();
            }
            catch (Exception)
            {
            }
        }

        public FlatStore(string Path, Type DefaultFormat)
            : this()
        {
            filePath = Path;

            if (defaultFormat == null)
            {
                defaultFormat = typeof(XmlSerFormat);
            }

            // Initialise the default format

            this.formats.Add(defaultFormat, (IFlatFormat)GCache.GetWeakItemByType(defaultFormat, null, true));
        }

        /// <summary>
        /// Allows the store to be loaded asynchronously with the ability to be informed when done
        /// </summary>
        /// <param name="create">Whether to create the store if it doesn't exist</param>
        /// <param name="callback">The callback to use when done, if <c>null</c> the event StoreLoaded is used</param>
        public void BeginLoad(bool create = true, Action callback = null)
        {
            ThreadPool.QueueUserWorkItem((state) =>
                {
                    Load(create);
                    if (callback != null)
                    {
                        callback();
                    }
                    else
                    {
                        if (StoreLoaded != null)
                        {
                            StoreLoaded(this, new EventArgs());
                        }
                        else
                        {
                            // We have no means of signalling that we're done, this should be a problem
                            throw new Exception("Asynchronous loading of store completed, but no means of signalling was registered");
                        }
                    }
                });
        }

        public void Load(bool create = true, bool useEvent = false)
        {
            if (fStream != null)
            {
                throw new Exception("Cannot load datastore, file is already open");
            }

            if (string.IsNullOrEmpty(filePath))
            {
                throw new Exception("Cannot load or create store as path isn't set");
            }

            if (File.Exists(filePath) == false)
            {
                if (create == false)
                {
                    throw new Exception("Store doesn't exist on disk at path [{0}], create set to false".UFormat(filePath));
                }
                else
                {
                    try
                    {
                        File.Create(filePath).Dispose();
                        Created = true;
                    }
                    catch (Exception ex)
                    {
                        throw new Exception("Failed to create store at path [{0}] - {1}".UFormat(filePath, ex.Message), ex);
                    }
                }
            }

            _load();

            if (useEvent && StoreLoaded != null)
            {
                StoreLoaded(this, new EventArgs());
            }
        }

        public void Commit()
        {
            string cLine = string.Empty;

            // Check there are any dirty entries before even bothering

            if (items.Any(item => item.Dirty) == false)
                return;

            using (fStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
            {
                sReader = new StreamReader(fStream);
                sWriter = new StreamWriter(fStream);
                sWriter.AutoFlush = true;

                // Go to the beginning
                fStream.Seek(0, SeekOrigin.Begin);

                // Write the type list (destructively if need be)
                sWriter.WriteLine(GenerateTypeList(this.types.ToArray()));

                // Write the format list (destructively if need be)
                sWriter.WriteLine(GenerateTypeList(this.formats.Select(form => form.Key).ToArray()));

                // get the remaining data from the file, this is just the data entries
                List<string> remaining = new List<string>();

                while (sReader.EndOfStream == false)
                {
                    remaining.Add(sReader.ReadLine());
                }

                // Get each of the dirty item's in the items collection
                foreach (DataItem dirtyItem in items.Where(item => item.Dirty))
                {
                    // Perform processing should this entry potentially be here already but needs updating
                    if (remaining.Count > 0)
                    {
                        string currentEntry = remaining.Select(entry => entry.Split(':')[1].Split(' ')[0]).DefaultIfEmpty(
                            null).FirstOrDefault(entry => entry.Trim() == dirtyItem.ID.ToString());

                        
                        dirtyItem.DATA = dirtyItem.Format.Decompose(dirtyItem.Type, dirtyItem.Instance);
                        string item = DataItemAsString(dirtyItem);

                        if (currentEntry == null)
                        {
                            sWriter.WriteLine(item);
                            dirtyItem.LastWrite = DateTime.Now;
                            dirtyItem.Dirty = false;
                        }
                        else
                        {
                            // This item has a pre-existing counterpart, it needs modifying
                        }
                    }
                    else
                    {
                        // There aren't any entries in the file already
                        // generate the data
                        dirtyItem.DATA = dirtyItem.Format.Decompose(dirtyItem.Type, dirtyItem.Instance);
                        string item = DataItemAsString(dirtyItem);

                        sWriter.WriteLine(item);

                        dirtyItem.LastWrite = DateTime.Now;
                        dirtyItem.Dirty = false;
                    }

                }



            }
        }

        private string DataItemAsString(DataItem di)
        {
            return string.Format("^{0}^{1}:{2} {3}",
                this.types.IndexOf(di.Type),
                this.formats.Select(item => item.Key).ToList().IndexOf(di.Format.GetType()),
                di.ID.ToString(),
                di.DATA);
        }

        private void _load()
        {
            try
            {
                fStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
                sReader = new StreamReader(fStream);
                sWriter = new StreamWriter(fStream);
                this.items.Clear(); // Empties the in-memory collection
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to load data store - {0}".UFormat(ex.Message), ex);
            }

            try
            {
                string line = string.Empty;
                int iLine = 0;
                while (sReader.EndOfStream == false)
                {
                    line = sReader.ReadLine();

                    if (iLine == 0)
                    {
                        // First line is type list

                        _processTypeList(line);
                    }
                    else if (iLine == 1)
                    {
                        // Second line is formatters list

                        _processFormatList(line);
                    }

                    if (line.Trim() == string.Empty)
                    {
                        continue;   // Skip over any blank lines
                    }

                    switch (line[0])
                    {
                        case '^':       // Data Item
                            _processDataItem(line);
                            break;
                    }

                    iLine++;
                }

                // Reset stream point
                fStream.Seek(0, SeekOrigin.Begin);
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to load file [{0}] - {1}".UFormat(filePath, ex.Message), ex);
            }
            finally
            {
                fStream.Close();
                fStream = null;
                sReader = null;
                sWriter = null;
            }
        }

        private void _processDataItem(string line)
        {
            if (string.IsNullOrEmpty(line))
            {
                throw new Exception("Cannot process line as data item, line cannot be null or empty");
            }

            try
            {
                // Get details

                int firstWhitespace = line.IndexOf(' ');
                string directive = line.Substring(0, firstWhitespace - 1);
                string[] pieces = directive.Split(':');
                int type = Convert.ToInt32(pieces[0].Split('^')[1]);
                int format = Convert.ToInt32(pieces[0].Split('^')[2]);
                string id = pieces[1];

                // Check the type and format exist (crude, purely based on index being available)

                if (types.Count > type == false)
                {
                    throw new Exception("Error loading Data Item with type ID [{0}], there aren't this many types".UFormat(type));
                }

                if (formats.Count > format == false)
                {
                    throw new Exception("Error loading Data Item with format ID [{0}], there aren't this many formats".UFormat(format));
                }

                //// Initialise the formatter if it hasn't been done

                //if (this.formats.Values.ElementAt(format) == null)
                //{
                //    Type formatType = this.formats.Keys.ElementAt(format);

                //    //this.formats[formatType] = (IFlatFormat)Activator.CreateInstance(formatType);

                //    this.formats[formatType] = (IFlatFormat)GCache.GetWeakItemByType(formatType);

                //}

                // Create the data item

                DataItem dItem = new DataItem();
                // Set the data item's owner
                dItem.Owner = this;

                // If our format has been initialised then we can set it
                if (formats.Values.ElementAt(format) != null)
                {
                    dItem.Format = formats.Values.ElementAt(format);
                }
                else
                {
                    // It hasn't been initialised yet, so just provide the Data Item with the necessary type
                    // to initialise it in the future
                    this.formats[formats.Keys.ElementAt(format)] = 
                        (IFlatFormat)GCache.GetWeakItemByType(formats.Keys.ElementAt(format));

                    dItem.Format = formats.Values.ElementAt(format);
                }

                dItem.ID = Convert.ToInt32(id);
                dItem.Dirty = false;

                // Now we have an ID we need to reserve it specially in the domain

                Unique.TryExternalReserve(dItem.ID, domainForInstance);

                //dItem.ID = id;
                dItem.Type = this.types[type];
               // dItem.Format = this.formats.Values.ElementAt(format);
                dItem.DATA = line.Substring(firstWhitespace + 1, (line.Length - firstWhitespace) - 1);

                this.items.Add(dItem);
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to process data item - {0}".UFormat(ex.Message), ex);
            }
        }

        private void _processTypeList(string line)
        {
            try
            {
                this.types.Clear();
                Type[] typelist = line.Split('>').AsParallel().AsOrdered().Distinct().Select(t => Type.GetType(t)).ToArray();
                foreach (Type t in typelist)
                {
                    if (t == null)
                    {
                        throw new Exception("Data type cannot be null, type must have changed");
                    }

                    this.types.Add(t);
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to process type list - {0}".UFormat(ex.Message), ex);
            }
        }

        private void _processFormatList(string line)
        {
            try
            {
                // Clear all formats other that the default at position 0
                for (int i = 1; i < formats.Count; i++)
                {
                    formats.Remove(formats.ElementAt(i).Key);
                }
                Type[] forms = line.Split('>').AsParallel().AsOrdered().Distinct().Select(t => Type.GetType(t)).ToArray();
                foreach (Type t in forms)
                {
                    if (t == null)
                    {
                        throw new Exception("Format type cannot be null, type must have changed");
                    }

                    // Don't overwrite any pre-existing format entries
                    if (this.formats.ContainsKey(t) == false)
                    {
                        this.formats.Add(t, null);
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to process format list - {0}".UFormat(ex.Message), ex);
            }
        }

        public int Insert<T, TFormat>(T inst) where TFormat : IFlatFormat
        {
            int nId = Unique.GetRandomInt32(domainForInstance);

            if (this.types.Contains(typeof(T)) == false)
            {
                this.types.Add(typeof(T));
            }

            if (this.formats.ContainsKey(typeof(TFormat)) == false)
            {
                this.formats.Add(typeof(TFormat), GCache.GetWeakItemByType<TFormat>(null, true));
            }

            DataItem di = new DataItem();
            di.ID = Unique.GetRandomInt32(domainForInstance);
            di.Format = this.formats[typeof(TFormat)];
            di.Owner = this;
            di.Type = typeof(T);
            di.Instance = inst;

            this.items.Add(di);

            return nId;
        }

        public int Insert<T>(T inst)
        {
            int nId = Unique.GetRandomInt32(domainForInstance);

            if (this.types.Contains(typeof(T)) == false)
            {
                this.types.Add(typeof(T));
            }

            DataItem di = new DataItem();
            di.ID = Unique.GetRandomInt32(domainForInstance);
            di.Format = this.formats.ElementAt(0).Value;
            di.Owner = this;
            di.Type = typeof(T);
            di.Instance = inst;

            this.items.Add(di);

            return nId;
        }

        public IEnumerable<T> GetByType<T>()
        {
            return items.Where(item => item.Type.Equals(typeof(T))).Select(item => (T)item.Instance);
        }

        public static string GenerateTypeList(params Type[] types)
        {
            var sb = new StringBuilder();
            foreach (Type t in types.AsParallel().AsOrdered().Distinct())
            {
                sb.Append(t.AssemblyQualifiedName);
                sb.Append(">");
            }

            return sb.ToString().Remove(sb.Length - 1, 1);
        }

        [DebuggerDisplay("ID:{ID}")]
        internal class DataItem
        {
            private object _inst = null;

            public int ID { get; internal set; }
            public Type Type { get; internal set; }
            public IFlatFormat Format { get; internal set; }
            public string DATA { get; internal set; }
            internal FlatStore Owner { get; set; }

            internal DateTime LastWrite { get; set; }
            internal bool Dirty { get; set; }

            public object Instance
            {
                get
                {
                    if (_inst == null)
                    {
                        _inst = Format.Compose(Type, DATA);
                    }

                    return _inst;
                }
                internal set
                {
                    if (_inst != value)
                    {
                        _inst = value;
                        Dirty = true;
                    }
                }
            }
        }
    }
}